Java Main Method & JVM Memory Model
Table of Contents
1. The main Method Deep Dive
Why is main Special?
The main
method is the entry point of any standalone Java application. When you run java MyClass
, the JVM specifically looks for this exact signature:
public static void main(String[] args)
If missing: JVM throws Error: Main method not found in class MyClass
Breakdown of Each Keyword
1. public
- Purpose: JVM must access it from outside the class
- Impact: If
private
orprotected
→ runtime error
// ❌ This won't work
private static void main(String[] args) {
System.out.println("Hello World");
}
// JVM cannot access private methods
2. static
- Purpose: JVM calls main without creating class instance
- Impact: Belongs to class, not instance
// ✅ JVM can call directly
public class MyClass {
public static void main(String[] args) {
// No need for: MyClass obj = new MyClass();
System.out.println("Hello World");
}
}
3. void
- Purpose: main doesn't return anything
- Impact: JVM just exits after execution
// ❌ JVM wouldn't know what to do with return value
public static int main(String[] args) {
return 42; // What should JVM do with this?
}
4. main
- Purpose: Method name JVM is hardcoded to find
- Impact: Execution always starts from main
5. String[] args
- Purpose: Holds command-line arguments
- Variations allowed:
String args[]
String... args
(varargs)
public class Demo {
public static void main(String[] args) {
System.out.println("Args length: " + args.length);
if (args.length > 0) {
System.out.println("First arg: " + args[0]);
}
}
}
// Run: java Demo hello world
// Output:
// Args length: 2
// First arg: hello
Method Overloading with main
public class MainOverload {
// ✅ JVM will call this one
public static void main(String[] args) {
System.out.println("Main with String[] args");
main("custom"); // Can call overloaded version
}
// ✅ Valid overload, but JVM won't call it directly
public static void main(String arg) {
System.out.println("Overloaded main: " + arg);
}
}
2. JVM Execution Process
Complete Flow: From Source to Execution
Your Code (.java)
↓ javac (compilation)
Bytecode (.class)
↓ java (execution)
JVM → Class Loader → Bytecode Verifier → Execution Engine
↓
Calls public static void main(String[] args)
↓
Your Program Runs 🎉
Step-by-Step Process
Step 1: Compilation
javac MyClass.java
# Creates MyClass.class (platform-independent bytecode)
Step 2: Execution Launch
java MyClass hello world
# Launches JVM with command-line arguments
Step 3: Class Loading
// Class Loader Subsystem loads classes in this order:
// 1. BootStrap Loader → core Java classes (java.lang.String)
// 2. Extension Loader → extended libraries
// 3. Application Loader → your classes (MyClass)
Step 4: JVM Verification
- Bytecode Verifier checks for:
- Legal bytecode instructions
- No memory access violations
- Type safety compliance
Step 5: Execution Engine
- Interpreter: Executes instructions line by line
- JIT Compiler: Converts frequently used code to native machine code
Step 6: Finding & Calling main
// JVM searches for exact signature
public static void main(String[] args)
// If found, JVM calls:
MyClass.main(new String[]{"hello", "world"});
3. JVM Memory Areas
Memory Structure Overview
┌─────────────────────────────────────────────────────────┐
│ JVM Memory │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Method Area │ │ Heap │ │Stack(Thread)│ │
│ │(Metaspace) │ │ │ │ │ │
│ │- Class Info │ │- Objects │ │- Local Vars │ │
│ │- Static Vars│ │- String Pool│ │- Method │ │
│ │- Bytecode │ │- Arrays │ │ Frames │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
Detailed Memory Areas
1. Method Area (Metaspace in Java 8+)
Stores: Class-level data
- Class definitions and metadata
- Static variables and methods
- Method bytecode
- Constant pool
2. Heap
Stores: Objects and instance data
- All object instances
- Instance variables
- Arrays
- String Pool (for string literals)
3. Stack (Per Thread)
Stores: Method execution data
- Local variables
- Method parameters
- Return addresses
- References to heap objects
Memory Example with Code
public class MemoryDemo {
private static int staticVar = 100; // Method Area
public static void main(String[] args) {
int x = 10; // Stack
String s1 = "Hello"; // String Pool (Heap)
String s2 = new String("World"); // Heap
Person p = new Person("John"); // Heap
processData(x, s1);
}
static void processData(int num, String text) {
// New stack frame created
int result = num * 2; // Stack
System.out.println(text + ": " + result);
}
}
class Person {
private String name; // Instance variable (Heap)
public Person(String name) {
this.name = name;
}
}
Memory Layout for Above Code
Method Area:
├── MemoryDemo class metadata
├── Person class metadata
├── staticVar = 100
└── Method bytecodes (main, processData, Person constructor)
Heap:
├── String Pool: "Hello"
├── new String("World") object
├── Person object { name: "John" }
└── String object "John" (for Person's name)
Stack (main thread):
┌─────────────────────┐
│ processData() frame │
│ - num = 10 │
│ - text → "Hello" │
│ - result = 20 │
└─────────────────────┘
┌─────────────────────┐
│ main() frame │
│ - args[] │
│ - x = 10 │
│ - s1 → "Hello" │
│ - s2 → new "World" │
│ - p → Person object │
└─────────────────────┘
Stack Frame Lifecycle
public class StackExample {
public static void main(String[] args) {
System.out.println("1. main() starts - frame pushed");
methodA();
System.out.println("4. Back in main() - methodA frame popped");
}
static void methodA() {
System.out.println("2. methodA() starts - frame pushed");
methodB();
System.out.println("3. Back in methodA() - methodB frame popped");
}
static void methodB() {
System.out.println("3. methodB() executing - top frame");
}
}
// Stack Evolution:
// Step 1: [main()]
// Step 2: [main()] → [methodA()]
// Step 3: [main()] → [methodA()] → [methodB()]
// Step 4: [main()] → [methodA()]
// Step 5: [main()]
4. Multithreading & Memory Model
Thread Memory Isolation
Key Principle: Each thread gets its own stack, but all threads share the same heap and method area.
Thread-1 Stack Thread-2 Stack Shared Memory
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│Local vars │ │Local vars │ │ Heap │
│Method frames│ │Method frames│ │ Objects │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ ├─────────────┤
│Method Area │
│ Static vars │
└─────────────┘
Thread Safety Example
public class ThreadSafetyDemo {
private static int sharedCounter = 0; // Shared in Method Area
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Each thread has its own stack with local variables
for (int i = 0; i < 1000; i++) { // 'i' is thread-local
sharedCounter++; // RACE CONDITION - shared resource
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
// Expected: 2000, Actual: Often less due to race condition
System.out.println("Final Counter: " + sharedCounter);
}
}
Race Condition Problem
// What happens during sharedCounter++:
// 1. READ current value from memory
// 2. INCREMENT the value
// 3. WRITE back to memory
// If both threads execute simultaneously:
Thread-1: READ (0) → INCREMENT (1) → WRITE (1)
Thread-2: READ (0) → INCREMENT (1) → WRITE (1)
// Result: 1 instead of 2 (lost update)
Solutions to Race Conditions
Solution 1: Synchronized Block
public class SynchronizedDemo {
private static int counter = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) { // Only one thread at a time
counter++;
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Counter: " + counter); // Always 2000
}
}
Solution 2: AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // Atomic operation
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Counter: " + counter.get()); // Always 2000
}
}
Memory Model Summary
Memory Area | Access | Thread Safety | Contains |
---|---|---|---|
Stack | Per-thread | Thread-safe | Local variables, method parameters |
Heap | Shared | Needs synchronization | Objects, instance variables |
Method Area | Shared | Needs synchronization | Static variables, class metadata |
Key Takeaways
- Local variables are automatically thread-safe (stored in individual stacks)
- Shared objects in heap require synchronization
- Static variables are shared across all threads
- Race conditions occur when multiple threads access shared mutable state
- Synchronization mechanisms (synchronized, atomic classes) ensure thread safety
Quick Reference
Main Method Checklist
- ✅
public static void main(String[] args)
- ✅ Exact signature required by JVM
- ✅ Entry point of application
- ✅ Can be overloaded but JVM calls String[] version
Memory Areas
- 🏗️ Method Area: Class definitions, static variables
- 🏠 Heap: Objects, instance variables (shared)
- 📚 Stack: Local variables, method frames (per-thread)
Thread Safety Rules
- 🔒 Stack variables: Thread-safe automatically
- ⚠️ Heap objects: Need synchronization
- 🚨 Static variables: Need synchronization